---
description:
  Learn how to implement pagination with Apollo client in both numbered pages and cursor-based
  approaches. Easy to use helper functions provided!
---

# Pagination

Sometimes, you will have one or more views in your application where you need to display a list that
contains too much data to be either fetched or displayed at once. Pagination is the most common
solution to this problem, and Apollo Client has built-in functionality that makes it quite easy to
do.

There are basically two ways of fetching paginated data: numbered pages, and cursors. There are also
two ways for displaying paginated data: discrete pages, and infinite scrolling. For a more in-depth
explanation of the difference and when you might want to use one vs. the other, we recommend that
you read our blog post on the subject:
[Understanding Pagination](https://blog.apollographql.com/understanding-pagination-rest-graphql-and-relay-b10f835549e7).

In this article, we'll cover the technical details of using Apollo to implement both approaches.

## Offset-Based

Offset based pagination - also called numbered pages - is a very common pattern, found on many
websites, because it is usually the easiest to implement on the backend. In SQL for example,
numbered pages can easily be generated by using
[OFFSET and LIMIT](https://www.postgresql.org/docs/8.2/static/queries-limit.html).

### The `offsetLimitPagination` Helper

Apollo Client provides an `offsetLimitPagination` helper function that you can use to generate a
field policy for every relevant list field.

This example uses `offsetLimitPagination` to generate a field policy for `Query.posts`:

```typescript
import { InMemoryCache } from '@apollo/client';
import { offsetLimitPagination } from '@apollo/client/utilities';

const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        feed: offsetLimitPagination(),
      },
    },
  },
});
```

### Using `fetchMore`

Apollo lets you do pagination with a method called
[`fetchMore`](../caching/interaction#incremental-loading-fetchmore). You need to specify what query
and variables to use for the update, and how to merge the new query result with the existing data on
the client. How exactly you do that will determine what kind of pagination you are implementing.

Here is an example with numbered pages taken from
[here](https://stackblitz.com/edit/simple-apollo-angular-example-fetch-more):

```ts
import { Apollo, gql, QueryRef } from 'apollo-angular';

const feedQuery = gql`
  query Feed($offset: Int, $limit: Int) {
    currentUser {
      login
    }
    posts(offset: $offset, limit: $limit) {
      id
      # ...
    }
  }
`;

@Component({
  // ...
})
class FeedComponent implements OnInit {
  apollo: Apollo;
  feedQuery: QueryRef<any>;
  feed: any[];
  itemsPerPage: number = 10;

  ngOnInit() {
    this.feedQuery = this.apollo.watchQuery<any>({
      query: feedQuery,
      variables: {
        offset: 0,
        limit: this.itemsPerPage,
      },
      fetchPolicy: 'network-only',
    });

    this.feed = this.feedQuery.valueChanges.subscribe(({ data }) => {
      this.feed = data.feed;
    });
  }

  fetchMore() {
    this.feedQuery.fetchMore({
      // query: ... (you can specify a different query. feedQuery is used by default)
      variables: {
        offset: this.feed.length,
      },
    });
  }
}
```

As you can see, `fetchMore` is accessible through the `QueryRef` object.

In the example above, `fetchMore` is a function which calls `fetchMore` with the length of the
current feed as a variable. Whenever you don't pass a query argument to `fetchMore`, fetch more will
use the original `query` again with new variables. Once the new data is returned from the server,
the `merge` function is used to merge it with the existing data, which will cause a re-render of
your UI component.

In the example above, the `fetchMore` function is called from the UI component:

```html
@if (loading) {
<loading />
} @else { @for (entry of feed; track $index) {
<feed-entry [entry]="entry" [currentUser]="currentUser" (onVote)="onVote($event)" />
}
<a (click)="fetchMore()">Load more</a>
}
```

One downside of pagination with numbered pages or offsets is that an item can be skipped or returned
twice when items are inserted into or removed from the list at the same time. That can be avoided
with cursor-based pagination.

### Setting `keyArgs` with `offsetLimitPagination`

If a paginated field accepts arguments besides `offset` and `limit`, you might need to specify the
key arguments that indicate whether two result sets belong to the same list or different lists.

To set `keyArgs` for the field policy generated by `offsetLimitPagination`, provide an array of
argument names to the function as a parameter:

```ts
fields: {
  // Results belong to the same list only if both the type
  // and userId arguments match exactly
  posts: offsetLimitPagination(['type', 'userId']);
}
```

## Cursor-Based

In cursor-based pagination a cursor is used to keep track of where in the data set the next items
should be fetched from. Sometimes the cursor can be quite simple and just refer to the ID of the
last object fetched, but in some cases - for example lists sorted according to some criteria - the
cursor needs to encode the sorting criteria in addition to the ID of the last object fetched.
Cursor-based pagination isn't all that different from offset-based pagination, but instead of using
an absolute offset, it points to the last object fetched and contains information about the sort
order used. Because it doesn't use an absolute offset, it is more suitable for frequently changing
datasets than offset-based pagination.

In the example below, we use a `fetchMore` query to continuously load new comments, which then
appear at the top. The cursor to be used in the `fetchMore` query is provided in the initial server
response, and has to be updated whenever more data is fetched.

```ts
const moreComments = gql`
  query moreComments($cursor: String) {
    moreComments(cursor: $cursor) {
      cursor
      comments {
        author
        text
      }
    }
  }
`;

class FeedComponent {
  feedQuery: QueryRef<any>;

  static cursor: any;

  // ...
  fetchMore() {
    this.feedQuery.fetchMore({
      query: moreComments,
      variables: {
        // cursor is the initial cursor returned by the original query
        // this.cursor is the cursor that we update via `updateQuery` below
        cursor: FeedComponent.cursor,
      },
    });
  }
  // ...
}
```

```ts
import { InMemoryCache } from '@apollo/client';

const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        feed: {
          keyArgs: false,

          merge(existing, incoming, { args: { cursor }, readField }) {
            const merged = existing ? existing.slice(0) : [];
            let offset = offsetFromCursor(merged, cursor, readField);
            // If we couldn't find the cursor, default to appending to
            // the end of the list, so we don't lose any data.
            if (offset < 0) offset = merged.length;
            // Now that we have a reliable offset, the rest of this logic
            // is the same as in offsetLimitPagination.
            for (let i = 0; i < incoming.length; ++i) {
              merged[offset + i] = incoming[i];
            }
            return merged;
          },

          // If you always want to return the whole list, you can omit
          // this read function.
          read(existing, { args: { cursor, limit = existing.length }, readField }) {
            if (existing) {
              let offset = offsetFromCursor(existing, cursor, readField);
              // If we couldn't find the cursor, default to reading the
              // entire list.
              if (offset < 0) offset = 0;
              return existing.slice(offset, offset + limit);
            }
          },
        },
      },
    },
  },
});

function offsetFromCursor(items, cursor, readField) {
  // Search from the back of the list because the cursor we're
  // looking for is typically the ID of the last item.
  for (let i = items.length - 1; i >= 0; --i) {
    const item = items[i];
    // Using readField works for both non-normalized objects
    // (returning item.id) and normalized references (returning
    // the id field from the referenced entity object), so it's
    // a good idea to use readField when you're not sure what
    // kind of elements you're dealing with.
    if (readField('id', item) === cursor) {
      // Add one because the cursor identifies the item just
      // before the first item in the page we care about.
      return i + 1;
    }
  }
  // Report that the cursor could not be found.
  return -1;
}
```

## Relay-Style Cursor Pagination

The `InMemoryCache` field policy API allows for any conceivable style of pagination, even though
some simpler approaches have known drawbacks. Apollo Client comes with a support for
[Cursor Connections Specification](https://facebook.github.io/relay/graphql/connections.htm).

You can check
[an example on Stackblitz](https://stackblitz.com/edit/apollo-angular-relay-cursor-pagination?file=app%2Flist.component.ts).

```ts
const COMMENTS_QUERY = gql`
  query Comments($cursor: String) {
    comments(first: 10, after: $cursor) {
      edges {
        node {
          author
          text
        }
      }
      pageInfo {
        endCursor
        hasNextPage
      }
    }
  }
`;

class FeedComponent {
  feedQuery: QueryRef<any>;

  static cursor: any;

  // ...
  fetchMore() {
    this.feedQuery.fetchMore({
      query: moreComments,
      variables: {
        // cursor is the initial cursor returned by the original query
        // this.cursor is the cursor that we update via `updateQuery` below
        cursor: FeedComponent.cursor,
      },
    });
  }
  // ...
}
```

Whenever you need to consume a Relay pagination API using Apollo Client, `relayStylePagination` is a
great tool to try first, even if you end up copy/pasting its code and making changes to suit your
specific needs.

```ts
import { relayStylePagination } from '@apollo/client/utilities';

const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        comments: relayStylePagination(),
      },
    },
  },
});
```
